由缓冲区表示的数据不能直接访问，必须创建访问器对象进行访问。访问器告知运行时希望在何处以及如何访问数据，从而允许运行时确保正确的数据在正确的时间出现在正确的位置。这是一个非常强大的概念，特别是与任务图结合使用时，任务图基于数据依赖来调度内核的执行。\par

访问器类有5个模板参数。第一个参数是访问数据的类型，这应该与对应缓冲区中存储的数据类型相同。第二个参数描述了数据和缓冲区的维度，默认值为1。\par

\hspace*{\fill} \par %插入空行
图7-6 访问模式
\begin{table}[H]
	\begin{tabular}{|l|l|}
		\hline
		\multicolumn{1}{|c|}{模式} & \multicolumn{1}{c|}{描述}               \\ \hline
		read                       & 只能读取                               \\ \hline
		write                      & 只能写，保留以前的内容 \\ \hline
		read\_write                & 读写都可以                          \\ \hline
	\end{tabular}
\end{table}

接下来的三个模板参数对访问器的设置。第一个是访问模式，描述了如何在程序中使用访问器。支持的模式如图7-6所示。我们将在第8章了解如何使用这些模式来安排内核的执行和数据移动。如果没有指定或自动推断出访问模式参数，则访问模式参数具有默认值。如果不指定，对于非const数据类型，访问器默认为read\_write访问模式，对于const数据类型，默认为read。这些默认值没有问题，但提供更准确的信息可以提高运行时执行优化的能力。开始应用程序开发时，不指定访问模式是安全而简洁的，然后可以根据对应用程序性能关键区域的分析来细化访问模式。\par

\hspace*{\fill} \par %插入空行
图7-7 访问目标
\begin{table}[H]
	\begin{tabular}{|l|l|}
		\hline
		\multicolumn{1}{|c|}{目标} & \multicolumn{1}{c|}{描述}        \\ \hline
		global\_buffer               & 通过全局内存访问一个缓冲区   \\ \hline
		constant\_buffer             & 通过常量内存访问缓冲区 \\ \hline
		local                        & 访问工作组本地内存          \\ \hline
		unsampled\_image             & 访问unsampled\_image              \\ \hline
		sampled\_image               & 访问sampled\_image                 \\ \hline
		host\_buffer                 & 访问主机上的缓冲区             \\ \hline
		host\_unsampled\_image       & 访问host\_unsampled\_image  \\ \hline
		host\_sampled\_image         & 访问host\_sampled\_image      \\ \hline
	\end{tabular}
\end{table}

下一个模板参数是访问目标。缓冲区是数据的抽象，所以隐藏了数据存储的位置和方式。访问目标既描述了正在访问的数据类型，也描述了哪些内存将包含该数据，可访问目标如图7-7所示。数据类型是两种类型中的一种:缓冲区或图像。本书中讨论了图像，可以把它们看作是为图像处理提供特定领域的缓冲区。\par

另一方面，设备可能有不同类型的内存，这些内存由不同的地址空间表示，常用的内存类型是设备的全局内存。内核中的大多数访问器都会使用这个目标，所以global是默认的目标(如果没有指定)。常量和本地缓冲区使用特殊用途的内存，常量内存用于存储内核内的常量值，本地内存是工作组可用的特殊内存，其他工作组不能访问。我们将在第9章了解如何使用本地内存。另一个需要注意的是主机缓冲区，访问主机上的缓冲区时使用的目标。这个模板形参的默认值是global\_buffer，所以在大多数情况下，不需要在代码中指定目标。\par

最终模板形参决定访问器是否为占位符访问器，这不是开发者直接设置的参数。占位符访问器在命令组之外声明，但用于访问内核内设备上的数据。通过创建访问器的例子，将了解占位符访问器与非占位符访问器的区别。\par

虽然使用缓冲区对象的get\_access方法从缓冲区对象中提取访问器，但直接创建(构造)更简单。接下来的例子会非常简单，并且容易理解。\par

\hspace*{\fill} \par %插入空行
\textbf{创建访问器}

图7-8显示了一个示例程序，其中包含了使用访问器所需的所有内容。本例中，有三个缓冲区：A、B和C。我们提交给队列的第一个任务是为每个缓冲区创建访问器和定义内核，内核使用这些访问器初始化缓冲区。每个访问器都用的是缓冲区的引用，以及由开发者提交给队列的命令组定义的处理程序对象构造的。这有效地将访问器绑定为命令组的一部分进行提交。常规访问器是设备访问器，默认情况下，它们的目标是存储在设备内存中的全局缓冲区。这是最常见的情况。\par

\hspace*{\fill} \par %插入空行
图7-8 简单的访问器创建
\begin{lstlisting}[caption={}]
constexpr int N = 42;

queue Q;

// create 3 buffers of 42 ints
buffer<int> A{range{N}};
buffer<int> B{range{N}};
buffer<int> C{range{N}};
accessor pC{C};

Q.submit([&](handler &h) {
	accessor aA{A, h};
	accessor aB{B, h};
	accessor aC{C, h};
	h.parallel_for(N, [=](id<1> i) {
		aA[i] = 1;
		aB[i] = 40;
		aC[i] = 0;
	});
});

Q.submit([&](handler &h) {
	accessor aA{A, h};
	accessor aB{B, h};
	accessor aC{C, h};
	h.parallel_for(N, [=](id<1> i) {
		aC[i] += aA[i] + aB[i]; });
});

Q.submit([&](handler &h) {
	h.require(pC);
	h.parallel_for(N, [=](id<1> i) {
		pC[i]++; });
});

host_accessor result{C};
for (int i = 0; i < N; i++)
	assert(result[i] == N);
\end{lstlisting}

提交的第二个任务还定义了三个缓冲区访问器。然后，第二个内核中使用这些访问器将缓冲区A和B的元素添加到缓冲区C中。因为第二个任务与第一个任务操作相同的数据，所以运行时将在第一个任务完成后执行。\par

第三个任务展示了如何使用占位符访问器。创建缓冲区之后，图7-8中的示例的开头声明访问器pC。请注意，构造函数没有传递处理程序对象，因为没有要传递的处理程序对象，可以提前创建一个可重用的访问器对象。然而，为了在内核中使用访问器，需要在提交时将它绑定到一个命令组，使用处理程序对象的require方法可以实现。将占位符访问器绑定到命令组时，就可以像使用其他访问器一样，在内核中使用它了。\par

最后，创建一个host\_accessor对象，以便在主机上读取计算结果。注意，这与内核中使用的类型不同。主机访问器使用单独的host\_accessor类来推断模板参数，并提供简单的对外接口。注意，本例中的主机访问器结果不接受处理程序对象，因为我们没有要传递的处理程序对象。主机访问器的特殊类型，允许将它们与占位符区分。主机访问器的一个要点是，只有当数据在主机上可用时，构造函数才会完成，主机访问器的构造可能需要很长时间。构造函数必须等待所需数据的内核上执行完毕，并通过复制数据完成构造。当主机访问器构造完成时，就可以使用它直接访问主机上的数据，并且可以保证在主机上使用的是最新的数据。\par

虽然示例完全正确，但在创建访问器时，并没有说明如何使用。对于缓冲区中的非const int数据，使用默认的访问模式(即读写模式)，可能过于保守，可能在操作之间产生不必要的依赖关系或多余的数据移动。如果运行时能够提供更多关于如何使用访问器的信息，可能会更好。但在介绍这样做的示例之前，应该首先介绍另一个工具——访问标识。\par

访问标识是表达访问器所需的访问模式和目标组合的一种方式。使用访问标记时，将作为参数传递给访问器的构造函数。可能的标识如图7-9所示。当使用标识参数构造访问器时，CTAD可以正确地推导出所需的访问模式和目标，从而提供简单的方法来覆盖这些模板参数的默认值。我们也可以手动指定所需的模板参数，标识提供了一种更简单、更紧凑的方式来获得相同的结果，而无需拼写出完全模板化的访问器。\par

\hspace*{\fill} \par %插入空行
图7-9 访问标识
\begin{table}[H]
	\begin{tabular}{|l|l|l|l|}
		\hline
		\textbf{标识类型}             & \textbf{标识值} & \textbf{访问模式} & \textbf{访问目标} \\ \hline
		\textbf{mode\_tag\_t}         & read\_write        & read\_write          & default                \\ \hline
		\textbf{mode\_tag\_t}         & read\_only         & read                 & default                \\ \hline
		\textbf{mode\_tag\_t}         & write\_only        & write                & default                \\ \hline
		\textbf{mode\_target\_tag\_t} & read\_constant     & read                 & constant\_buffer       \\ \hline
	\end{tabular}
\end{table}

让我们以前面的示例为例，重写以添加访问标识。这个改进示例如图7-10所示。\par

\hspace*{\fill} \par %插入空行
图7-10 使用指定使用方式创建访问器
\begin{lstlisting}[caption={}]
constexpr int N = 42;

queue Q;

// Create 3 buffers of 42 ints
buffer<int> A{range{N}};
buffer<int> B{range{N}};
buffer<int> C{range{N}};

accessor pC{C};

Q.submit([&](handler &h) {
	accessor aA{A, h, write_only, noinit};
	accessor aB{B, h, write_only, noinit};
	accessor aC{C, h, write_only, noinit};
	h.parallel_for(N, [=](id<1> i) {
		aA[i] = 1;
		aB[i] = 40;
		aC[i] = 0;
	});
});

Q.submit([&](handler &h) {
	accessor aA{A, h, read_only};
	accessor aB{B, h, read_only};
	accessor aC{C, h, read_write};
	h.parallel_for(N, [=](id<1> i) {
		aC[i] += aA[i] + aB[i]; });
});

Q.submit([&](handler &h) {
	h.require(pC);
	h.parallel_for(N, [=](id<1> i) {
		pC[i]++; });
});

host_accessor result{C, read_only};

for (int i = 0; i < N; i++)
	assert(result[i] == N);
\end{lstlisting}

首先声明缓冲区，如图7-8所示。我们还创建了占位符访问器，现在看看提交给队列的第一个任务。之前，通过传递对缓冲区的引用和命令组的处理程序对象来创建访问器。现在，向构造函数调用添加两个参数。第一个参数是一个访问标识，因为这个内核正在为缓冲区写入初始值，所以使用write\_only访问标记。这让运行时知道这个内核正在生成新数据，从而不会从缓冲区中读取数据。\par

第二个参数是一个可选的访问器属性，类似于本章前面看到的缓冲区的可选属性。我们传递的属性noinit，让运行时知道缓冲区前面的内容可以丢弃，可以让运行时消除不必要的数据移动。本例中，由于第一个任务是为缓冲区写入初始值，所以运行时没有必要在内核执行之前将未初始化的主机内存复制到设备中。noinit属性对于这个示例很有用，但是不应该用于读-改-写，或者内核函数只更新缓冲区中的一些值的情况。\par

我们提交给队列的第二个任务与之前的任务相同，但是现在向访问器添加了访问标识。向访问器A和B添加read\_only标识，以让运行时知道我们只会通过这些访问器读取缓冲区A和B的值。第三个访问器aC为read\_write访问标识，因为我们将A和B的元素之和累积到c中。我们在示例中显式地使用标记以保持一致，但这没必要，因为默认的访问模式就是read\_write。\par

使用占位符访问器的第三个任务中保留了默认用法，这与图7-8中看到的简化示例一样。最终访问器，即主机访问器结果，在创建时接收到一个访问标识。因为我们只读取主机上的最终值，所以将read\_only标记传递给构造函数。如果改写程序，这样主机访问器销毁，启动另一个内核缓冲区C操作时，当使用read\_only标识时，就不需要写回到设备端了，并且运行时知道主机不会修改这个缓冲区。\par

\hspace*{\fill} \par %插入空行
\textbf{可以用访问器做什么?}

使用访问器对象可以完成很多事情，最重要的是通过访问器的[]操作符可以完成对数据的访问。在图7-8和7-10的例子中使用[]操作符，其可以接受索引多维数据的id对象或单个size\_t，当访问器有多个维度时使用第二种方式。在查询到达标量值之前，它会返回一个对象，该对象可以再次使用[]进行索引。二维情况下，可以表示为a[i][j]，访问器维度的排序遵循C++的约定。\par

访问器还可以返回指向底层数据的指针，这个指针可以按照C++规则直接访问。对于这个指针的地址空间，可能会比较复杂。地址空间及其特殊性将在后面的章节中讨论。\par

还可以通过访问器对象查询许多内容。示例包括通过访问器可访问的元素数量、所覆盖的缓冲区区域的字节大小或可访问的数据范围。\par

访问器提供了与C++容器类似的接口，可以在许多可能传递容器的情况下使用。访问器支持的容器接口包括data方法(相当于get\_pointer)，以及几种不同类型的前向迭代器和后向迭代器。\par










































